/**
 * 基于 element-plus vClickOutside 改写的 popover 层外部元素点击指令
 */
import { isClient } from '@vueuse/core'
import type { ObjectDirective } from 'vue'

type Handlers = {
	domHandler: (el: MouseEvent) => void
	bindingFn: (e: MouseEvent) => void
}

/**
 * 注册了指令的 dom 元素
 */
const nodeList = new Map<Element, Handlers[]>()

/**
 * 在浏览器环境中，给 dom 添加 mouseup 事件，触发 click-popover-outside 指令对 dom 的处理
 */
if (isClient) {
	document.addEventListener('mouseup', e => {
		for (const handlers of nodeList.values()) {
			for (const { domHandler } of handlers) {
				domHandler(e)
			}
		}
	})
}

/**
 * 创建指令处理 dom 元素的方式
 * @param el vue 指令钩子参数的 el
 * @param binding vue 指令钩子参数的 binding
 */
const createDomHandler = (
	el: HTMLElement,
	binding: DirectiveBinding<(e: MouseEvent) => void>
) => {
	return (e: MouseEvent) => {
		// 鼠标事件的目标元素
		const target = e.target as HTMLElement | null
		// 目标元素是否在排除元素内
		let isExclude = false
		/**
		 * 检查 exclude 修饰符 v-click-popover-outside.exclude
		 * 当修饰符 exclude 为 true，且存在指令参数时，检查排除元素
		 * @example v-click-popover-outside:id1:id2.exclude
		 * 检查鼠标事件目标元素是否在 id 对应的元素内部
		 */
		if (binding.modifiers.exclude) {
			const arg = binding.arg
			let excludeElementIds: string[]
			excludeElementIds = (arg && arg.split(':')) as string[]
			if (excludeElementIds && excludeElementIds.length > 0) {
				for (const id of excludeElementIds) {
					const el = document.getElementById(id)
					if (el) {
						isExclude = el.contains(target)
						if (isExclude) break
					}
				}
			}
		}
		// 检查鼠标事件目标元素是否在指令绑定的元素内
		const isContained = el.contains(target)
		// 在指令绑定的元素内或在排除元素内，不触发指令绑定的回调函数
		if (isContained || isExclude) {
			return
		}
		binding.value(e)
	}
}
export const clickPopoverOutside: ObjectDirective<
	HTMLElement,
	(e: MouseEvent) => void
> = {
	beforeMount(el, binding) {
		if (!nodeList.has(el)) {
			nodeList.set(el, [])
		}
		nodeList.get(el)!.push({
			domHandler: createDomHandler(el, binding),
			bindingFn: binding.value
		})
	},
	updated(el, binding) {
		if (!nodeList.has(el)) {
			nodeList.set(el, [])
		}
		const handlers = nodeList.get(el)
		const oldHandlerIndex = handlers!.findIndex(
			item => item.bindingFn === binding.oldValue
		)
		const newHandler = {
			domHandler: createDomHandler(el, binding),
			bindingFn: binding.value
		}
		if (oldHandlerIndex >= 0) {
			handlers!.splice(oldHandlerIndex, 1, newHandler)
		} else {
			handlers!.push(newHandler)
		}
	},
	unmounted(el) {
		nodeList.delete(el)
	}
}
